package primitives;

import java.util.ArrayList;
import java.util.List;
import java.util.Map.Entry;

import utility.Intersection;
import utility.Matrix4x4;
import utility.Vector3;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;

import engine.Core;
import engine.Hemicube;

public class Patch {
	
	public Vector3[] vertices;
	public Shape parentShape;
//	public int color;
	public Vector3 normal;
	
	public double emission;
	public double incident = 0;
	public double reflectance = 0;
	public double excident = 0;
	
	public Hemicube hemi;
	
	public List<Patch> children;
	
	public Patch(Vector3[] vertices, Shape parentShape) {
		if(vertices == null) {
			throw new IllegalArgumentException("Patch's vectors can't be null.");
		}
		if(vertices.length != 3) {
			throw new IllegalArgumentException("Patch must have 3 vertices.");
		}
		this.vertices = vertices;
		this.parentShape = parentShape;
		this.normal = normal();
	}
	
	public Vector3[] getVectors() {
		return vertices;
	}
	
	
	/**
	 *  p0____*______p2
	 *   |   m2|    /
	 *   |   / |   /
	 *   |  /  |  /
	 *   | /   | /
	 *   |/____|/
	 *   *m1   * m3
	 *   |    /
	 *   |   /
	 *   |  /
	 *   | /
	 *   p1
	 *   
	 */
	public List<Patch> subdivide(int timesToSubdivide) {
		if(timesToSubdivide == 0) {
			return ImmutableList.of(this);
		} else {
			List<Patch> patches = new ArrayList<Patch>(4);
			List<Patch> leaves = new ArrayList<Patch>();
			Vector3 midpoint1 = Vector3.midpoint(vertices[0], vertices[1]);
			Vector3 midpoint2 = Vector3.midpoint(vertices[0], vertices[2]);
			Vector3 midpoint3 = Vector3.midpoint(vertices[1], vertices[2]);
			patches.add(new Patch(new Vector3[]{vertices[0], midpoint1, midpoint2}, parentShape));
			patches.add(new Patch(new Vector3[]{midpoint3, midpoint2, midpoint1},   parentShape));
			patches.add(new Patch(new Vector3[]{midpoint2, midpoint3, vertices[2]}, parentShape));
			patches.add(new Patch(new Vector3[]{midpoint1, vertices[1], midpoint3}, parentShape));
			for(Patch patch : patches) {
	//			patches.get(i).color = this.color;
				patch.emission = this.emission;
				patch.reflectance = this.reflectance;
				leaves.addAll(patch.subdivide(timesToSubdivide - 1));
			}
			children = patches;
			return leaves;
		}
	}
	
	/*
	 * Gives normal to patch's plane.
	 * @param inside True if normal should face "inside" Shape, false otherwise
	 */
	public Vector3 normal() {
		Vector3 normal;
		Vector3 v1 = vertices[1].minus(vertices[0]);
		Vector3 v2 = vertices[2].minus(vertices[0]);
		if(parentShape instanceof Room) {
			normal = v2.crossProduct(v1);
		} else {
			normal = v1.crossProduct(v2);
		}
		for(int i = 0; i < normal.elements.length; i++) {
			if(Math.abs(normal.elements[i]) < 0.000001) {
				normal.elements[i] = 0.0;
			}
		}
		normal.normalize();
		return normal;
	}
	
	public Intersection<Patch,Double> intersects(Vector3 eyePosition, Vector3 pixel) {
		double t = intersectsPlane(eyePosition, pixel);
		if(Double.isInfinite(t)) {
			return new Intersection<Patch,Double>(null, Double.NaN);
		}
		Vector3 intersectPt = pointOfIntersection(eyePosition, pixel, t);
		Patch intersectPatch = intersectsPatch(intersectPt);
		if(intersectPatch == null) {
			return new Intersection<Patch,Double>(null, Double.NaN);
		} else {
			return new Intersection<Patch,Double>(intersectPatch, t);
		}
	}
	
	@VisibleForTesting
	double intersectsPlane(Vector3 eyePosition, Vector3 pixel) {
		Vector3 direction = pixel.minus(eyePosition);
		Vector3 normal = normal();
		double planeSum = normal.dotProduct(vertices[0]);
		double tCoefficient = normal.dotProduct(direction);
		planeSum = planeSum - normal.dotProduct(eyePosition);
		double t = planeSum / tCoefficient;
		return t;
	}
	
	public Patch intersectsPatch(Vector3 intersectPt) {
		Vector3 u = vertices[1].minus(vertices[0]);
		Vector3 v = vertices[2].minus(vertices[0]);
		Vector3 w = intersectPt.minus(vertices[0]);
		
		double denom = u.dotProduct(u) * v.dotProduct(v) - 
				u.dotProduct(v) * u.dotProduct(v);
		double r = (v.dotProduct(v) * u.dotProduct(w) - 
				u.dotProduct(v) * v.dotProduct(w)) / denom;
		double s = (u.dotProduct(u) * v.dotProduct(w) - 
				u.dotProduct(v) * u.dotProduct(w)) / denom;
		if(r >= 0 && s >= 0 && (r + s) <= 1) {
			if(children == null) {
				return this;
			} else {
				Patch smallest = null;
				for(int i = 0; i < children.size(); i++) {
					Patch child = children.get(i).intersectsPatch(intersectPt);
					if(child != null) {
						smallest = child;
					}
				}
				return smallest;
			}
		} else {
			return null;
		}
	}
	
	@VisibleForTesting
	Vector3 pointOfIntersection(Vector3 eyePosition, Vector3 pixel, double t) {
		Vector3 direction = pixel.minus(eyePosition);
		Vector3 intersection = new Vector3(new double[]{eyePosition.x() + direction.x() * t,
				eyePosition.y() + direction.y() * t, eyePosition.z() + direction.z()*t});
		return intersection;
	}
	
	public void setColor(int color) {
	//	this.color = color;
	}
	
	public Vector3 getCenterPoint() {
		double x = (vertices[0].x() + vertices[1].x() + vertices[2].x()) / 3.0;
		double y = (vertices[0].y() + vertices[1].y() + vertices[2].y()) / 3.0;
		double z = (vertices[0].z() + vertices[1].z() + vertices[2].z()) / 3.0;
		return new Vector3(new double[]{x, y, z});
	}
	
	public void setHemicube() {
		this.hemi = new Hemicube(this, Core.FACES_WIDE);
	}

	public void gather() {
		double incident = 0;
		double formFactors = 0;
		for (Entry<Patch,Double> patchJ : hemi.getFormFactors().entrySet()) {
			formFactors += patchJ.getValue();
			incident += patchJ.getKey().excident * patchJ.getValue();
		}
		if(formFactors > 1.0001 || formFactors < 0.9999) {
			//System.out.println(formFactors + ", reflectance = " + reflectance);
		}
		this.incident = incident;
	}
	
	public void shoot() {
		this.excident = emission + incident * reflectance;
	}
	
	public Matrix4x4 getTransformMatrix() {
		return parentShape.getTransformMatrix();
	}
}
